﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Data;
using System.Data.Linq;
using Microsoft.Forums.OfflineStore;
using Threading = System.Threading;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Collections;
using System.Globalization;
using Microsoft.Forums.ForumsProvider;
using System.Diagnostics;

namespace Microsoft.Forums.SyncService
{
    /// <summary>
    /// Controlls the synchronization of data retrieved from Forum Service providers with the local database
    /// </summary>
    public class SynchronizationController : ISynchronizationController
    {
        private string databaseConnectionString;
        private IConnectionPool connectionPool;
        private IDictionary<Guid, IForumServiceProvider> serviceProviders = new Dictionary<Guid, IForumServiceProvider>();

        /// <summary>
        /// Dictionary for coordinating asynchronous operations
        /// </summary>
        private Dictionary<Guid, AsyncOperation> synchronizationState = new Dictionary<Guid, AsyncOperation>();

        /// <summary>
        /// Delegate type used to launch a synchronization asynchronously
        /// </summary>
        /// <param name="idToSync"></param>
        /// <param name="asyncOp"></param>
        private delegate void WorkerEventHandler(Guid idToSync, AsyncOperation asyncOp);

        /// <summary>
        /// Delegate to use with AsyncOperationManager once a async operation is 
        /// done to  marshall is back to the original calling thread
        /// </summary>
        private Threading.SendOrPostCallback onSynchronizeForumCompletedDelegate;

        /// <summary>
        /// Delegate to use with AsyncOperationManager once a async operation is 
        /// done to  marshall is back to the original calling thread
        /// </summary>
        private Threading.SendOrPostCallback onSynchronizeThreadCompletedDelegate;

        /// <summary>
        /// Event fire when a synchronization of a forum is complete
        /// </summary>
        public event EventHandler<SynchronizeForumCompletedEventArgs> SynchronizeForumCompleted;

        /// <summary>
        /// Event fire when a synchronization of a thread is complete
        /// </summary>
        public event EventHandler<SynchronizeThreadCompletedEventArgs> SynchronizeThreadCompleted;

        /// <summary>
        /// Create the Synchronization Controller using a database connections tring
        /// </summary>
        /// <param name="connectionString">The connection string.</param>
        /// <param name="providers">The providers.</param>
        public SynchronizationController(string connectionString,
                                         IList<IForumServiceProvider> providers)
        {
            if (String.IsNullOrEmpty(connectionString)) throw new ArgumentNullException("connectionString", "connectionString must not be null or empty");
            if (!(new ForumsOfflineStore(connectionString).DatabaseExists()))
                throw new SynchronizationDatabaseException("Database does not exist");

            databaseConnectionString = connectionString;
            Initialize(providers);
        }

        /// <summary>
        /// Create the Synchronization Controller using a connection pool
        /// </summary>
        /// <param name="pool">The connection poolpool.</param>
        /// <param name="providers">The providers.</param>
        public SynchronizationController(IConnectionPool pool,
                                         IList<IForumServiceProvider> providers)
        {
            if (pool == null) throw new ArgumentNullException("pool", "pool must not be null");
            connectionPool = pool;
            Initialize(providers);
        }

        #region Private Methods
        /// <summary>
        /// Set up the synchronization controller
        /// </summary>
        /// <param name="connectionString">The connection string.</param>
        /// <param name="providers">List of service providers</param>
        /// <exception cref="System.ArgumentNullException"/>
        /// <exception cref="Microsoft.Forums.SyncService.ForumNotInDatabaseException"/>
        private void Initialize(IList<IForumServiceProvider> providers)
        {
            if (providers == null) throw new ArgumentNullException("providers", "providers must not be null");

            InitializeDelegates();
            InitializeForumServiceProviders(providers);
        }

        /// <summary>
        /// Initialize the delegates we use for events during synchronization
        /// </summary>
        private void InitializeDelegates()
        {
            onSynchronizeForumCompletedDelegate =
                new Threading.SendOrPostCallback(OnSynchronizeForumCompleted);

            onSynchronizeThreadCompletedDelegate +=
                new Threading.SendOrPostCallback(OnSynchronizeThreadCompleted);
        }


        /// <summary>
        /// Scans the list of forum services and makes sure they are present in the
        /// ForumServiceProvider table in the local store.  If not, add them.
        /// </summary>
        /// <param name="providers">A collection of providers</param>
        private void InitializeForumServiceProviders(IList<IForumServiceProvider> providers)
        {
            foreach (var provider in providers)
            {
                AddForumServiceProvider(provider);
            }
        }

        /// <summary>
        /// Adds a ForumServiceProvider to the database
        /// </summary>
        /// <param name="provider">The provider to add to database</param>
        private void AddForumServiceProvider(IForumServiceProvider provider)
        {
            if (!serviceProviders.ContainsKey(provider.ProviderId))
            {
                serviceProviders.Add(provider.ProviderId, provider);

                var db = GetNewDatabaseContext();
                if (!db.ForumServiceProviders.Any(fsp => fsp.Id == provider.ProviderId))
                {
                    ForumServiceProvider fpv = new ForumServiceProvider();
                    fpv.DisplayName = provider.ProviderName;
                    fpv.Id = provider.ProviderId;
                    fpv.ProviderType = provider.ProviderName;
                    db.ForumServiceProviders.InsertOnSubmit(fpv);

                    db.SubmitChanges();
                }
            }
        }

        /// <summary>
        /// Get an instance of of the database context.  We want to use a different context for each group of operations
        /// since each context will track its changes inorder to supply optimistic concurency
        /// </summary>
        /// <returns></returns>
        private ForumsOfflineStore GetNewDatabaseContext()
        {
            if (connectionPool != null)
            {
                return new ForumsOfflineStore(connectionPool.Connection);
            }
            else
            {
                return new ForumsOfflineStore(databaseConnectionString);
            }

        }


        /// <summary>
        /// Cancel an asynchronous operation
        /// </summary>
        /// <param name="forumId">Forum id to check if canceled</param>
        /// <returns>True if canceled</returns>
        private bool TaskCanceled(object forumId)
        {
            return !synchronizationState.ContainsKey((Guid)forumId);
        }


        /// <summary>
        /// This method is called on a background thread and corrdinated executing the synchronization
        /// </summary>
        /// <param name="forumId">Id of forum to synchronize</param>
        /// <param name="asyncOp"></param>
        private void SyncForumAsync(Guid forumId, AsyncOperation asyncOp)
        {
            Exception e = null;
            SynchronizeForumSummary forumSummary = null;

            // Check that the task is still active.
            // The operation may have been canceled before
            // the thread was scheduled.
            if (!TaskCanceled(asyncOp.UserSuppliedState))
            {
                try
                {
                    // Call the method to sync but pass in a predicate which will check if cancelation occurs
                    forumSummary = SyncForum(forumId, id => TaskCanceled(id));
                }
                catch (Exception ex)
                {
                    e = ex;
                }
            }

            // Synchronization is finished so 
            // clean up the asynchronous state and package results by calling
            // SynchronizeCompletion
            SynchronizeForumCompletion(
                forumId,
                e,
                TaskCanceled(asyncOp.UserSuppliedState),
                asyncOp,
                forumSummary);

        }


        /// <summary>
        /// Called when an asynchronous synchronization of a whole forum is completed
        /// Responsible for packaging up results and sending it back to the main thread
        /// </summary>
        /// <param name="exception">Any exception that occured</param>
        /// <param name="canceled">Whether or not this async operation was canceled</param>
        /// <param name="asyncOp">The async operation object used for marshalling</param>
        /// <param name="forumSummary">Summary of what was synchronized</param>
        private void SynchronizeForumCompletion(Guid forumid, Exception exception, bool canceled, AsyncOperation asyncOp, SynchronizeForumSummary forumSummary)
        {
            // If the task was not previously canceled,
            // remove the task from the lifetime collection.
            if (!canceled)
            {
                lock (synchronizationState)
                {
                    synchronizationState.Remove((Guid)asyncOp.UserSuppliedState);
                }
            }

            SynchronizeForumCompletedEventArgs e =
                new SynchronizeForumCompletedEventArgs(
                    forumid,
                    forumSummary,
                    exception,
                    canceled,
                    asyncOp.UserSuppliedState
                );

            // End the task. The asyncOp object is responsible 
            // for marshaling the call.
            asyncOp.PostOperationCompleted(onSynchronizeForumCompletedDelegate, e);
        }

        /// <summary>
        /// When synchronization for a whole forum has completed this method is called to 
        /// fire the SynchronizeForumCompleted event
        /// </summary>
        /// <param name="operationState"></param>
        private void OnSynchronizeForumCompleted(object operationState)
        {
            SynchronizeForumCompletedEventArgs e =
                operationState as SynchronizeForumCompletedEventArgs;

            if (SynchronizeForumCompleted != null)
            {
                SynchronizeForumCompleted(this, e);
            }
        }


        /// <summary>
        /// Called when an asynchronous synchronization of a thread is completed
        /// Responsible for packaging up results and sending it back to the main thread
        /// </summary>
        /// <param name="forumid">The forumid.</param>
        /// <param name="threadId">The thread id.</param>
        /// <param name="exception">Any exception that occured</param>
        /// <param name="asyncOp">The async op.</param>
        /// <param name="threadSummary">The thread summary.</param>
        private void SynchronizeThreadCompletion(Guid forumid, Guid threadId, Exception exception, AsyncOperation asyncOp, SynchronizeThreadSummary threadSummary)
        {
            SynchronizeThreadCompletedEventArgs e =
                new SynchronizeThreadCompletedEventArgs(
                    forumid,
                    threadId,
                    threadSummary,
                    exception
                );

            asyncOp.Post(onSynchronizeThreadCompletedDelegate, e);
        }

        /// <summary>
        /// When synchronization for a thread has completed this method is called to 
        /// fire the SynchronizeThreadCompleted event
        /// </summary>
        /// <param name="operationState"></param>
        private void OnSynchronizeThreadCompleted(object operationState)
        {
            SynchronizeThreadCompletedEventArgs e =
                operationState as SynchronizeThreadCompletedEventArgs;

            if (SynchronizeThreadCompleted != null)
            {
                SynchronizeThreadCompleted(this, e);
            }
        }

        /// <summary>
        /// Synchronize a forum sent from the gui.
        /// 
        /// Must be called from SyncForumAsync or SynchronizeForum , since they do the validation
        /// </summary>
        /// <param name="syncForumId">The id of the forum to synchronize</param>
        /// <param name="shouldCancel">Predicate to determine if the operation was canceled</param>
        /// <returns>A summary object which indicates what was synchronized</returns>
        /// <exception cref="Microsoft.Forums.SyncService.SynchronizationException" />
        private SynchronizeForumSummary SyncForum(Guid syncForumId, Predicate<Guid> shouldCancel)
        {
            // Properties we will populate from forum object in the database
            IForumServiceProvider provider = null;
            List<SynchronizeThreadSummary> threadSummaries = new List<SynchronizeThreadSummary>();
            SynchronizeForumSummary forumSummary = new SynchronizeForumSummary(syncForumId, threadSummaries);

            // The time before we started Syncing
            DateTime timeBeforeSync = DateTime.Now.ToUniversalTime();

            // Will contain the date time to check for updates since
            DateTime updateFromDate;

            // This block is used to check if the syncForumId is valid and retrieve properties from it which we need inorder to synchronize
            using (var db = GetNewDatabaseContext())
            {
                var dbForum = db.Forums.Single(f => f.Id == syncForumId);
                if (!serviceProviders.ContainsKey(dbForum.ForumServiceProviderId)) throw new SynchronizationException(String.Format(CultureInfo.InvariantCulture, "No service provider exists for {0}", dbForum.Name));

                provider = serviceProviders[dbForum.ForumServiceProviderId];

                // Figure out what date we want to get all updates since
                if (dbForum.ForumPropertySet.LastSyncedOn != null)
                {
                    updateFromDate = (DateTime)dbForum.ForumPropertySet.LastSyncedOn;
                }
                else if (dbForum.ForumPropertySet.InitialSyncAge != null)
                {
                    // Since initialSyncAge is a number of days in the past, subtract from current date
                    updateFromDate = DateTime.Now.ToUniversalTime().AddDays(-1 * (double)dbForum.ForumPropertySet.InitialSyncAge);
                }
                else
                {
                    throw new SynchronizationException(String.Format(CultureInfo.InvariantCulture, "Forum {0} does not have InitialSyncAge or LastSyncedOn value", dbForum.Name));
                }
            }



            // We create a new database context so that we can attach and update existing entities retrieved from the web service layer 
            using (var db = GetNewDatabaseContext())
            {
                // Update forum even if not modified so that we can reset the LastSyncedOn time
                var updatedForum = provider.GetForum(syncForumId); // get potentially updated forum
                // Update forum object
                db.Forums.Attach(updatedForum);
                // We need to refresh so it sees the new values
                // This basically tells Linq to SQL that all values in our updated Forum are new
                db.Refresh(RefreshMode.KeepCurrentValues, updatedForum);

                // Only update threads if forum is modified
                if (updatedForum.ModifiedOn >= updateFromDate)
                {
                    Debug.WriteLine("Synchronizing forum: " + updatedForum.DisplayName);

                    // Get all threads since updateFrom until the time we began syncing
                    var threads = provider.GetUpdatedThreads(updatedForum.Id, updateFromDate, timeBeforeSync);
                    if (threads != null) // Do we have threads to update?
                    {
                        foreach (var thread in threads)
                        {

                            // Check if this asynchronous operation was canceled
                            if (shouldCancel != null && shouldCancel(syncForumId))
                            {
                                return null;
                            }

                            var threadSummary = SyncThreadHelper(thread, db, provider);
                            threadSummaries.Add(threadSummary);

                            Debug.WriteLine("Synchronized Thread # " + threadSummaries.Count + " from forum: " + updatedForum.DisplayName);
                        }
                    }
                }

                if (shouldCancel == null || !shouldCancel(syncForumId))
                {
                    // Update the last synced on date for the update Forum
                    updatedForum.ForumPropertySet.LastSyncedOn = timeBeforeSync;
                    db.SubmitChangesWithMerge();
                }


            }

            return forumSummary;

        }


        /// <summary>
        /// Synchronizes a threads posts given an existing datacontext
        /// This method is called from either of the private methods SyncThread or SyncForum
        /// This method is needed since both those function need this functionality
        /// 
        /// Preconditions: updatedThread != null, db != null, provider != null
        ///                updatedThread is not from a Data Contex, it must be retrieved from the web service layer
        /// </summary>
        /// <param name="updatedThread">Updated thread recieved from the webservice (NOT from the database/ data context)</param>
        /// <param name="db">An open datacontext</param>
        /// <param name="provider">A forum service provider for this thread</param>
        /// <returns>A summary of what was synchronized</returns>
        private SynchronizeThreadSummary SyncThreadHelper(Thread updatedThread, ForumsOfflineStore db, IForumServiceProvider provider)
        {
            // Time we began syncing this Thread
            DateTime beginTime = DateTime.Now.ToUniversalTime();
            SynchronizeType threadSyncType = SynchronizeType.Add;

            // Check if we have this thread already
            if (!db.Threads.Any(t => t.Id == updatedThread.Id))
            {
                // This is a new thread so we add it to database with a ThreadProperty object
                updatedThread.ThreadPropertySet = new ThreadPropertySet();
                db.Threads.InsertOnSubmit(updatedThread);
                threadSyncType = SynchronizeType.Add;
            }
            else
            {
                // We have this thread already so update local store
                db.Threads.Attach(updatedThread);
                // We need to refresh so it sees the new values
                db.Refresh(RefreshMode.KeepCurrentValues, updatedThread);
                threadSyncType = SynchronizeType.Update;
            }

            // At this point we are sure we have the updateThread object in our local database
            // Now update the posts 
            // If updatedThread.ThreadPropertySet.LastSyncedOn is null we get all posts
            // Other wise get all updated since that dade
            IList<Post> posts = null;

            // Collects post ids so return in summary object
            List<SynchronizePostSummary> postSummaries = new List<SynchronizePostSummary>();
            if (updatedThread.ThreadPropertySet.LastSyncedOn == null)
            {   // we never synchronized this thread before so get all posts
                posts = provider.GetAllPosts(updatedThread.ForumId, updatedThread.Id);
            }
            else if (updatedThread.ModifiedOn >= updatedThread.ThreadPropertySet.LastSyncedOn)
            {
                posts = provider.GetUpdatedPosts(updatedThread.ForumId, updatedThread.Id, (DateTime)updatedThread.ThreadPropertySet.LastSyncedOn, beginTime);
            }

            bool rootPostUpdated = false;
            if (posts != null) // try to update posts
            {
                foreach (var post in posts)
                {
                    
                    if (post.ParentId == null) // if this is a root post add thread modified date
                    {
                        post.ThreadModifiedOn = updatedThread.ModifiedOn;
                        rootPostUpdated = true;
                    }
                    postSummaries.Add(SyncPostHelper(post, db));
                }
            }

            if (!rootPostUpdated)
            {
                // we need to update the ThreadModifiedDate on the rootpost ourselves now since 
                // the rootpost did not come down in this update

                var rootPost = db.Posts.SingleOrDefault(p => p.Id == updatedThread.RootPostId);
                if (rootPost != null)
                {
                    postSummaries.Add(new SynchronizePostSummary(rootPost.ForumId,rootPost.ThreadId,rootPost.Id,SynchronizeType.Update));
                    rootPost.ThreadModifiedOn = updatedThread.ModifiedOn;
                }
            }

            updatedThread.ThreadPropertySet.LastSyncedOn = beginTime;

            SynchronizeThreadSummary summary = new SynchronizeThreadSummary(updatedThread.ForumId, updatedThread.Id, postSummaries, threadSyncType);

            db.SubmitChangesWithMerge();
            SynchronizeThreadCompletion(updatedThread.ForumId, updatedThread.Id, null, synchronizationState[updatedThread.ForumId], summary);

            return summary;
        }

        /// <summary>
        /// Synchronizes a post  given an existing datacontext
        /// This method MUST be called from SyncThreadHelper, it is seperated out for readability
        /// 
        /// Preconditions: updatedPost != null, db != null
        ///                updatedPost is not from a Data Contex, it must be retrieved from the web service layer
        /// </summary>
        /// <param name="updatedThread">Updated post recieved from the webservice (NOT from the database/ data context)</param>
        /// <param name="db">An open datacontext</param>
        private SynchronizePostSummary SyncPostHelper(Post updatedPost, ForumsOfflineStore db)
        {

            SynchronizeType postSyncType = SynchronizeType.Add;

            // Get the user objects and then replace objects with ids to stop multiple insertions
            User createdBy = updatedPost.DetachCreatedBy();
            User modifiedBy = updatedPost.DetachModifiedBy();

            // Sync the User objects seperate from the post
            SyncUserHelper(createdBy);
            if (modifiedBy != null)
            {
                SyncUserHelper(modifiedBy);
            }


            // Check if we have this post already
            if (!db.Posts.Any(p => p.Id == updatedPost.Id))
            {
                postSyncType = SynchronizeType.Add;

                // This is a new post so we add it to database with a PostProperty object
                updatedPost.PostPropertySet = new PostPropertySet();
                db.Posts.InsertOnSubmit(updatedPost);
            }
            else
            {
                postSyncType = SynchronizeType.Update;

                // We have this post already so update local store

                db.Posts.Attach(updatedPost);

                // We need to refresh so it sees the new values, since no one else should be writing to the
                // post table we consider all changes as new
                db.Refresh(RefreshMode.KeepCurrentValues, updatedPost);
            }

            return new SynchronizePostSummary(updatedPost.ForumId, updatedPost.ThreadId,updatedPost.Id, postSyncType);
        }

        /// <summary>
        /// Synchronizes a user  using a new datacontext.
        /// We use a new datacontext here since this user object could be updated from different forum synchronizations
        /// 
        /// Preconditions: updatedUser != null
        ///                updatedUser is not from a DataContex, it must be retrieved from the web service layer
        /// </summary>
        /// <param name="updatedThread">Updated user recieved from the webservice (NOT from the database/ data context)</param>
        private void SyncUserHelper(User updatedUser)
        {
            try
            {
                using (var db = GetNewDatabaseContext())
                {
                    // Set timestamp so that we can insure linq to sql picks add its to changes
                    updatedUser.Timestamp = DateTime.Now.ToUniversalTime();

                    // Check if we have this user already
                    if (!db.Users.Any(u => u.Id == updatedUser.Id))
                    {
                        db.Users.InsertOnSubmit(updatedUser);
                    }
                    else
                    {
                        // We have this user already so update local store
                        db.Users.Attach(updatedUser);
                    }

                    db.SubmitChangesWithMerge();
                }
            }
            catch (Exception e)
            {
                // This exception probably was caused be a race condition in threads adding users
                // This means this user was inserted right after we checked if it was
                // 
                // We would rather catch a more specific excpetion but there is none that is not sql
                // database provider specifc
                Debug.WriteLine(e);
            }

        }


        /// <summary>
        /// Determines whether [is forum in database] [the specified forum id].
        /// </summary>
        /// <param name="forumId">The forum id.</param>
        /// <returns>
        /// 	<c>true</c> if [is forum in database] [the specified forum id]; otherwise, <c>false</c>.
        /// </returns>
        private bool IsForumInDatabase(Guid forumId)
        {
            using (var db = GetNewDatabaseContext())
            {

                var forum = db.Forums.SingleOrDefault(f => f.Id == forumId);
                return forum != null;
            }
        }
        #endregion



        #region ISynchronizationController Members

        /// <summary>
        /// Retrieve a collection of the forums service provider we have available
        /// </summary>
        public IList<IForumServiceProvider> ServiceProviders
        {
            get { return new List<IForumServiceProvider>(serviceProviders.Values); }
        }


        /// <summary>
        /// Gets a collection of all the forums 
        /// </summary>
        /// <param name="brand"></param>
        /// <param name="locale"></param>
        /// <returns>List of available forums</returns>
        /// <exception cref="System.ArgumentNullException" />
        /// <exception cref="System.ArgumentException" />
        public IList<Forum> GetAvailableForums(string brand, int locale)
        {
            if (String.IsNullOrEmpty(brand)) throw new ArgumentNullException("brand", "brand must not be null");
            if (locale <= 0) throw new ArgumentException("locale must be greater than 0", "locale");

            List<Forum> availableForums = new List<Forum>();
            foreach (IForumServiceProvider prov in serviceProviders.Values)
            {
                availableForums.AddRange(prov.GetAvailableForums(brand, locale));
            }
            return availableForums;
        }

        /// <summary>
        /// Get a collection of all the supported brands
        /// </summary>
        /// <returns>collection of brands</returns>
        public IList<string> SupportedBrands
        {
            get
            {
                List<string> brands = new List<string>();
                foreach (IForumServiceProvider prov in serviceProviders.Values)
                {
                    brands.AddRange(prov.SupportedBrands);
                }
                return brands;
            }
        }

        /// <summary>
        /// Get a collection of all the supported locales
        /// </summary>
        /// <returns>collection of locales</returns>
        public IList<KeyValuePair<int, string>> SupportedLocales
        {
            get
            {
                List<KeyValuePair<int, string>> locales = new List<KeyValuePair<int, string>>();
                foreach (IForumServiceProvider prov in serviceProviders.Values)
                {
                    locales.AddRange(prov.SupportedLocales);
                }
                return locales;
            }
        }


        /// <summary>
        /// Adds a Forum to the local store.
        /// In order to call this you must pass in the Forum object you want to add and
        /// a ForumPropertySet object which describes the different properties for the Forum
        /// </summary>
        /// <param name="forum">The forum to subscribe to</param>
        /// <param name="propertySet">The user configurable properties for this forum</param>
        /// <exception cref="System.ArgumentNullException" />
        public void SubscribeToForum(Forum forum, ForumPropertySet propertySet)
        {

            if (forum == null) throw new ArgumentNullException("forum", "forum must not be null");
            if (propertySet == null) throw new ArgumentNullException("propertySet", "propertySet must not be null");

            var db = GetNewDatabaseContext();
            if (!db.Forums.Any(f => f.Id == forum.Id))
            {
                forum.ForumPropertySet = propertySet;
                db.Forums.InsertOnSubmit(forum);
                db.SubmitChangesWithMerge();
            }
        }


        /// <summary>
        /// Remove a Forum (and all associated Post data) from the local store
        /// </summary>
        /// <param name="forumId">The id of the forum to unsubscribe from</param>
        /// <exception cref="Microsoft.Forums.SyncService.SynchronizationInProgressException" />
        /// <exception cref="Microsoft.Forums.SyncService.ForumNotInDatabaseException" />
        public void UnsubscribeFromForum(Guid forumId)
        {
            lock (synchronizationState)
            {
                if (synchronizationState.ContainsKey(forumId))
                {
                    throw new SynchronizationInProgressException("Forum is already involed in a synchronization");
                }

                var db = GetNewDatabaseContext();
                var forumToDelete = db.Forums.SingleOrDefault(f => f.Id == forumId);

                if (forumToDelete != null)
                {
                    db.Forums.DeleteOnSubmit(forumToDelete);
                    db.SubmitChangesWithMerge();
                }
                else
                {
                    throw new ForumNotInDatabaseException("Forum does not exist");
                }
            }

        }


        /// <summary>
        /// Determines whether the specified forum is synchronizing.
        /// </summary>
        /// <param name="forumId">The forum id.</param>
        /// <returns>
        /// 	<c>true</c> if the specified forum  is synchronizing; otherwise, <c>false</c>.
        /// </returns>
        public bool IsSynchronizing(Guid forumId)
        {
            lock (synchronizationState)
            {
                return synchronizationState.ContainsKey(forumId);
            }
        }

        /// <summary>
        /// This method cancels an asynchronous operation give the forum id involved
        /// </summary>
        /// <param name="forumId">Id of forum involved in asynchrnous operation</param>
        public void CancelSynchronization(Guid forumId)
        {
            lock (synchronizationState)
            {
                if (synchronizationState.ContainsKey(forumId))
                {
                    synchronizationState.Remove(forumId);
                }
            }
        }

        /// <summary>
        /// Synchronize a specific forum in a synchronous(blocking) manner
        /// </summary>
        /// <param name="forumId">Id of the forum to synchronize</param>
        /// <exception cref="Microsoft.Forums.SyncService.SynchronizationInProgressException" />
        /// <exception cref="Microsoft.Forums.SyncService.ForumNotInDatabaseException" />
        /// <exception cref="Microsoft.Forums.SyncService.SynchronizationException" />
        /// <returns>A summary object which indicates what was synchronized</returns>
        public SynchronizeForumSummary SynchronizeForum(Guid forumId)
        {
            // Create an AsyncOperation for forumId.
            CreateAsyncOperation(forumId);
            SynchronizeForumSummary summary = SyncForum(forumId, id => TaskCanceled(id));
            lock (synchronizationState)
            {
                synchronizationState.Remove(forumId);
            }
            return summary;
        }


        /// <summary>
        /// Synchronize the given forum asynchronously.
        /// </summary>
        /// <param name="forumId">Id of the forum to synchronize</param>
        /// <exception cref="Microsoft.Forums.SyncService.SynchronizationInProgressException" />
        public void SynchronizeForumAsync(Guid forumId)
        {
            // Create an AsyncOperation for forumId.
            AsyncOperation asyncOp = CreateAsyncOperation(forumId);

            // Start the asynchronous operation.
            WorkerEventHandler workerDelegate = new WorkerEventHandler(SyncForumAsync);
            workerDelegate.BeginInvoke(
                forumId,
                asyncOp,
                null,
                null);
        }

        /// <summary>
        /// Creates the async operation.
        /// </summary>
        /// <param name="forumId">The forum id.</param>
        /// <returns></returns>
        private AsyncOperation CreateAsyncOperation(Guid forumId)
        {
            // Create an AsyncOperation for taskId.
            AsyncOperation asyncOp =
                AsyncOperationManager.CreateOperation(forumId);

            // Multiple threads will access the task dictionary,
            // so it must be locked to serialize access.
            lock (synchronizationState)
            {
                if (synchronizationState.ContainsKey(forumId))
                {
                    throw new SynchronizationInProgressException(
                        "Forum is already involved in a synchronization");
                }

                if (!IsForumInDatabase(forumId))
                {
                    throw new ForumNotInDatabaseException("Forum is not in the database");
                }

                synchronizationState[forumId] = asyncOp;
            }

            return asyncOp;
        }

        /// <summary>
        /// This deletes all data in that Forum which is stored locally.
        /// It will then synchronize all data for that Forum which has been updated since 
        /// the forum's SynchFrom value which is store in the database
        /// </summary>
        /// <param name="forum">Id of the forum to reset</param>
        public void ResetForum(Guid forumId)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Processes the pending action queue. Runs throughs each items in the queue and processes it
        /// and either updates its status or removes it from the queue
        /// </summary>
        public void ProcessPendingActionQueue()
        {
            using (ForumsOfflineStore db = GetNewDatabaseContext())
            {
                var actionsToProcess = from a in db.PendingActions
                                       where a.ActionStatus == (int)ActionStatus.Pending
                                       select a;
                foreach (PendingAction a in actionsToProcess)
                {
                    try
                    {
                        IForumServiceProvider provider = serviceProviders[a.ForumServiceProviderId];
                        switch ((ActionType)a.ActionType)
                        {
                            case ActionType.NewThread:
                                if (a.ThreadType != null && (ThreadType)a.ThreadType == ThreadType.Question)
                                {
                                    provider.CreateQuestionThread(a.ForumId, a.Topic, a.Body);
                                }
                                else if (a.ThreadType != null && (ThreadType)a.ThreadType == ThreadType.Comment)
                                {
                                    provider.CreateCommentThread(a.ForumId, a.Topic, a.Body);
                                }
                                else
                                {
                                    throw new NotSupportedException("Creation of thread Type " + ((ThreadType)a.ThreadType).ToString() + " is not supported");
                                }

                                break;

                            case ActionType.Reply:
                                if (a.ThreadId == null) throw new SynchronizationDatabaseException("ThreadId must not be null");
                                if (a.PostId == null) throw new SynchronizationDatabaseException("PostId must not be null");

                                provider.CreateReply((Guid)a.ThreadId, (Guid)a.PostId, a.Body);

                                break;

                            case ActionType.MarkAsAnswer:
                                if (a.PostId == null) throw new SynchronizationDatabaseException("PostId must not be null");

                                provider.MarkPostAsAnswer((Guid)a.PostId);

                                break;

                            case ActionType.UnmarkAsAnswer:
                                if (a.PostId == null) throw new SynchronizationDatabaseException("PostId must not be null");

                                provider.UnmarkPostAsAnswer((Guid)a.PostId);

                                break;

                            default:
                                throw new NotSupportedException("Action " + ((ActionType)a.ActionType).ToString() + " is not supported");

                        }

                        // If we get here the action succeeded so remove it from the pending action queue
                        db.PendingActions.DeleteOnSubmit(a);
                    }
                    catch (NotSupportedException)
                    {
                        // Something we don't support was called so send this exception out
                        a.ActionStatus = (int)ActionStatus.Failed;
                        throw;
                    }
                    catch (SynchronizationDatabaseException)
                    {
                        // We were unable to call provider methods since the data in the database is wrong
                        a.ActionStatus = (int)ActionStatus.Failed;
                        throw;
                    }
                    catch (Exception)
                    {
                        // Ideally we would catch a more specific exception here but we don't know what the
                        // API will throw
                        // If we get here we assume the user doesn't have permission to call this method or
                        // something went wrong in the calling of it. 

                        a.ActionStatus = (int)ActionStatus.Failed;
                    }
                }

                // After updates are complete submit changes
                db.SubmitChangesWithMerge();

            }

        }

        #endregion
    }
}
